AWSネットワークリソースの設定変更を Microsoft Teamsで複数ユーザーにメンション通知できるようにしてみた
こんにちは!AWS事業本部のおつまみです。
みなさん、セキュリティグループやルートテーブル、ネットワークACLなどネットワークリソースの変更・追加・削除を検知して、Teamsに通知してほしいなぁと思ったことはありますか?私はあります。
通知することで、不正や意図しない変更を迅速に把握し、対応することが可能になります。
以前こちらのブログにて、セキュリティグループの設定変更(変更・追加・削除)を通知する方法をお伝えしました。
今回上記のブログで展開したテンプレートにて、以下の追加要望を受けました。
- 複数ユーザーをメンション通知できるようにしてほしい。
- セキュリティグループ以外のネットワークリソースであるルートテーブルやネットワークACLの変更・追加・削除も通知してほしい。
この要望に沿って、CloudFormationテンプレートを修正したので、ご紹介します!
このような通知画面となります。
※画像では松波以外メンションになっていませんが、チーム部屋にそのユーザーがいればメンションされるようになります。
構成図
今回構築する構成です。
前提として、CloudTrailが既に有効化されている環境で構築します。
AWSネットワークリソースを変更(追加・削除)から、Teams通知するまでの流れをセキュリティグループの変更を例に説明いたします。
- セキュリティグループの変更(追加・削除):ユーザーまたはAWSサービスがセキュリティグループに対して変更(追加・削除)を実施
- CloudTrailによるログ記録:AWS CloudTrailが、この変更をイベントとして記録
- EventBridgeルールのトリガー:設定されたEventBridgeルール(このテンプレートのEventsRuleリソース)が、CloudTrailのイベントを検知
- Lambda関数の呼び出し:EventBridgeルールのターゲットとして設定されたLambda関数(
NotifyTeamsFunction
)が呼び出されます。イベントデータがLambda関数に渡されます。 - イベントデータの処理とメッセージ作成:Lambda関数内のコードが、受け取ったイベントデータを処理します。関数は以下の処理を行います:
- イベントの詳細(アカウントID、時間、リージョン、イベント名など)を抽出
- 変更されたリソースのタイプとIDを特定
- 設定されたユーザーリストからメンション用のテキストを生成
- Teamsに送信するためのメッセージ(Adaptive Card形式)を作成
- TeamsのWebhookへの送信:Lambda関数が、作成したメッセージをTeamsのWebhook URLにHTTP URLにPOSTリクエストを送信
- Teamsでの通知表示:TeamsがWebhookからのデータを受け取り、指定されたチャンネルに通知を表示
なお前回はLambdaではなく、EventBridgeルールでのInputTransformerを利用し、通知していました。
Teams通知までの流れは以下のとおりです。
1~3は同様
4. イベントデータの変換:EventBridgeルールのInputTransformerが、検知したイベントデータを、Teamsに送信するための適切な形式に変換
5. API Destinationの呼び出し:変換されたデータが、設定されたAPI Destination(EventsApiDestinationリソース)に送
6. TeamsのWebhookへの送信:API DestinationがTeamsのWebhook URLにPOSTリクエストを送信
7. Teamsでの通知表示:TeamsがWebhookからのデータを受け取り、指定されたチャンネルに通知を表示
CloudFormationテンプレートを準備
テンプレート内容は以下の通りです。
テンプレート
AWSTemplateFormatVersion: "2010-09-09"
Description: "nwresource-notify-teams"
Parameters:
SystemPrefix:
Type: String
EnvPrefix:
Type: String
AllowedValues:
- prd
- test
- dev
- poc
WebhookURL:
Type: String
MentionedUsers:
Type: String
Default: "[{\"email\":\"[email protected]\",\"name\":\"User1\"},{\"email\":\"[email protected]\",\"name\":\"User2\"}]"
Description: "JSON string of users to mention, e.g., '[{\"email\":\"[email protected]\",\"name\":\"User1\"},{\"email\":\"[email protected]\",\"name\":\"User2\"}]'"
Resources:
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: LambdaExecutionPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: arn:aws:logs:*:*:*
NotifyTeamsFunctionLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/aws/lambda/${SystemPrefix}-${EnvPrefix}-notify-teams-function"
RetentionInDays: 14
UpdateReplacePolicy: Retain
DeletionPolicy: Retain
NotifyTeamsFunction:
Type: AWS::Lambda::Function
DependsOn: NotifyTeamsFunctionLogGroup
Properties:
FunctionName: !Sub "${SystemPrefix}-${EnvPrefix}-notify-teams-function"
Handler: index.handler
Role: !GetAtt LambdaExecutionRole.Arn
Code:
ZipFile: |
import json
import urllib.request
import os
def handler(event, context):
webhook_url = os.environ['WEBHOOK_URL']
mentioned_users = json.loads(os.environ['MENTIONED_USERS'])
detail = event['detail']
account = event['account']
time = event['time']
region = event['region']
event_name = detail['eventName']
resource_id = detail.get('requestParameters', {}).get('groupId') or \
detail.get('responseElements', {}).get('groupId') or \
detail.get('requestParameters', {}).get('networkAclId') or \
detail.get('responseElements', {}).get('networkAcl', {}).get('networkAclId') or \
detail.get('requestParameters', {}).get('routeTableId') or \
detail.get('responseElements', {}).get('routeTable', {}).get('routeTableId') or \
'N/A'
resource_type = "セキュリティグループ" if "SecurityGroup" in event_name else \
"ネットワークACL" if "NetworkAcl" in event_name else \
"ルートテーブル" if "RouteTable" in event_name else \
"ネットワークリソース"
mentions = [f"\u003cat\u003e{user['name']}\u003c/at\u003e" for user in mentioned_users]
mentions_text = ", ".join(mentions)
message = {
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"type": "AdaptiveCard",
"body": [
{
"type": "TextBlock",
"text": f"{mentions_text}\n\n# **{resource_type}変更通知**\n\n{resource_type} : {resource_id} が変更されました。\n\n詳細:\n- アカウントID: **{account}**\n- 発生時間: **{time} (UTC)**\n- 変更ユーザー名: **{detail['userIdentity']['sessionContext']['sessionIssuer']['userName']}**\n- {resource_type}ID: **{resource_id}**\n- 検知API: **{event_name}**\n\n**詳細は以下のリンクからご確認ください。(検知してからイベント履歴確認まで15分程度時間がかかります。)**\n\n[CloudTrailイベントの詳細を確認する](https://{region}.console.aws.amazon.com/cloudtrailv2/home?region={region}#/events/{detail['eventID']})"
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.0",
"msteams": {
"width": "full",
"entities": [
{
"type": "mention",
"text": f"\u003cat\u003e{user['name']}\u003c/at\u003e",
"mentioned": {
"id": user['email'],
"name": user['name']
}
} for user in mentioned_users
]
}
}
}
]
}
req = urllib.request.Request(webhook_url)
req.add_header('Content-Type', 'application/json')
jsondata = json.dumps(message)
jsondataasbytes = jsondata.encode('utf-8')
req.add_header('Content-Length', len(jsondataasbytes))
response = urllib.request.urlopen(req, jsondataasbytes)
return {
'statusCode': response.getcode(),
'body': response.read().decode('utf-8')
}
Runtime: python3.12
Timeout: 30
Environment:
Variables:
WEBHOOK_URL: !Ref WebhookURL
MENTIONED_USERS: !Ref MentionedUsers
EventBridgeRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: events.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: InvokeLambdaFunction
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: !GetAtt NotifyTeamsFunction.Arn
EventsRuleSecurityGroup:
Type: AWS::Events::Rule
Properties:
Name: !Sub ${SystemPrefix}-${EnvPrefix}-sg-notify-teams
EventPattern:
detail-type:
- AWS API Call via CloudTrail
source:
- "aws.ec2"
detail:
eventSource: ["ec2.amazonaws.com"]
eventName:
- AuthorizeSecurityGroupIngress
- AuthorizeSecurityGroupEgress
- RevokeSecurityGroupIngress
- RevokeSecurityGroupEgress
- CreateSecurityGroup
- DeleteSecurityGroup
- ModifySecurityGroupRules
State: ENABLED
Targets:
- Arn: !GetAtt NotifyTeamsFunction.Arn
Id: EventsRuleSecurityGroup
EventBusName: default
EventsRuleNetworkACL:
Type: AWS::Events::Rule
Properties:
Name: !Sub ${SystemPrefix}-${EnvPrefix}-nacl-notify-teams
EventPattern:
detail-type:
- AWS API Call via CloudTrail
source:
- "aws.ec2"
detail:
eventSource: ["ec2.amazonaws.com"]
eventName:
- CreateNetworkAcl
- DeleteNetworkAcl
- ReplaceNetworkAclAssociation
- CreateNetworkAclEntry
- DeleteNetworkAclEntry
- ReplaceNetworkAclEntry
State: ENABLED
Targets:
- Arn: !GetAtt NotifyTeamsFunction.Arn
Id: EventsRuleNetworkACL
EventBusName: default
EventsRuleRouteTable:
Type: AWS::Events::Rule
Properties:
Name: !Sub ${SystemPrefix}-${EnvPrefix}-rt-notify-teams
EventPattern:
detail-type:
- AWS API Call via CloudTrail
source:
- "aws.ec2"
detail:
eventSource: ["ec2.amazonaws.com"]
eventName:
- CreateRoute
- DeleteRoute
- ReplaceRoute
- AssociateRouteTable
- DisassociateRouteTable
- ReplaceRouteTableAssociation
- CreateRouteTable
- DeleteRouteTable
- EnableVgwRoutePropagation
- DisableVgwRoutePropagation
- EnableTransitGatewayRouteTablePropagation
- DisableTransitGatewayRouteTablePropagation
State: ENABLED
Targets:
- Arn: !GetAtt NotifyTeamsFunction.Arn
Id: EventsRuleRouteTable
EventBusName: default
LambdaPermissionSecurityGroup:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref NotifyTeamsFunction
Action: lambda:InvokeFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt EventsRuleSecurityGroup.Arn
LambdaPermissionNetworkACL:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref NotifyTeamsFunction
Action: lambda:InvokeFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt EventsRuleNetworkACL.Arn
LambdaPermissionRouteTable:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref NotifyTeamsFunction
Action: lambda:InvokeFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt EventsRuleRouteTable.Arn
前回構成からの変更点
前回の構成から以下のような変更を行いました。
-
EventBridge ルールの変更:
- セキュリティグループだけでなく、ルートテーブルとネットワークACLの変更も検知するようにイベントパターンを拡張
-
InputTransformer から Lambda 関数への変更:
- 複雑な処理を可能にするため、InputTransformer の代わりに Lambda 関数を EventBridge ルールのターゲットとして設定
- InputTransformerは、イベントデータの単純な文字列置換には適していますが、複雑な処理を行うことはできません。
- 例えば、複数ユーザーのリストを動的に処理したり、条件に基づいて通知内容を変更したりすることは困難です。
- InputTransformerの機能と制限については、AWSの公式ドキュメント「Amazon EventBridge 入力変換」をご参考ください。
-
Lambda 関数の実装:
- 複数ユーザーへのメンション通知を処理するロジックを実装
- セキュリティグループ、ルートテーブル、ネットワークACLの変更を区別して適切なメッセージを生成するロジックを追加
-
IAM ロールの権限拡張:
- Lambda 関数が新たに追加されたリソース(ルートテーブル、ネットワークACL)の情報にアクセスできるよう、IAM ロールの権限を拡張
-
Teams通知の変更:
- 従来のInputTransformerの代わりに、LambdaをEventBridgeルールのターゲットとして指定
- イベントデータを受け取り、必要な処理を行った後、Microsoft Teamsに通知を送信する流れを実現
いざ検証
CloudFromationでデプロイ
パラメータは、以下の値を入れます。
- SystemPrefix
- システム名
- EnvPrefix
- 環境名
- WebhookURL
- TeamsのWebhookURL(取得方法は、こちらのブログをご参照ください。)
- MentionedUsers
- メンション先のメールアドレスとユーザー名
- 例
[{\"email\":\"[email protected]\",\"name\":\"User1\"},{\"email\":\"[email protected]\",\"name\":\"User2\"}]
3分ほどでデプロイが完了します。
ネットワークリソースを変更
セキュリティグループのインバウンドルールを編集してみます。
変更して数秒後、想定通りのTeams通知が届きました!
- セキュリティグループ変更時
同じようにルートテーブルやネットワークACLも変更してみます。
-
ルートテーブル削除時
-
ネットワークACL変更時
15分ほど経過後、Teamsに記載されているCloudTrailのリンクを選択します。ここからより詳細な情報が確認できますね。
メンション先を変更してみた
Lambda関数の環境変数を変更するだけで、メンションできるユーザーを変更できます。
ユーザーを1人だけに変更してみました。
同じようにセキュリティグループのインバウンドルールを編集してみます。
指定したユーザーにのみメンションされるようになりました。
最後に
今回は、AWSネットワークリソースの設定変更を Microsoft Teamsで複数ユーザーにメンション通知できるようにする方法をご紹介しました!
AWSネットワークリソースの設定変更を通知することで、意図しない設定変更を早期に発見できることができます。
お使いのAWS環境でぜひ実装してみてください!
以上、おつまみ(@AWS11077)でした!